﻿using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading;
using System.Diagnostics;
using log4net;
using Samurai.Validation;

namespace Samurai.Workflow
{
    /// <summary>
    /// Defines a workflow that executes steps in sequential order.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class SequentialWorkflow<T> : ISequentialWorkflow<T> where T : class, IWorkflowContext
    {
        private static readonly ILog Logger = LogManager.GetLogger(typeof (SequentialWorkflow<T>));
        private readonly Queue<IWorkflowStep<T>> _underlyingQueue;
        private bool _stopFlagSet;
        private IValidator<T> _contextValidator;
        private static object _uiThreadDispatcher;
        private static MethodInfo _dispatcherBeginInvokeMethodInfo;

        #region Constructors

        public SequentialWorkflow() : this(default(T)) {}

        /// <summary>
        /// Initializes a new instance of the <see cref="SequentialWorkflow&lt;T&gt;"/> class.
        /// </summary>
        /// <param name="context">The context.</param>
        public SequentialWorkflow(T context)
        {
            _underlyingQueue = new Queue<IWorkflowStep<T>>();
            Context = context;
        }

        #endregion

        #region Public Members

        /// <summary>
        /// Occurs when the final step in the workflow has completed.
        /// </summary>
        public event EventHandler<WorkflowCompletedEventArgs<T>> WorkflowCompleted;

        /// <summary>
        /// Occurs when a step in the workflow fails.
        /// </summary>
        public event EventHandler<WorkflowCompletedEventArgs<T>> WorkflowFailed;

        /// <summary>
        /// Gets or sets the context which to pass along the workflow as it executes.
        /// </summary>
        /// <value>The context.</value>
        public T Context { get; set; }

        /// <summary>
        /// Gets or sets the context validator.
        /// </summary>
        /// <value>The context validator.</value>
        public IValidator<T> ContextValidator
        {
            get
            {
                if (_contextValidator == null && Context != null)
                    _contextValidator = Validator<T>.Create(Context);

                return _contextValidator;
            }
            set { _contextValidator = value; }
        }

        /// <summary>
        /// Gets the steps that make up the current workflow instance.
        /// </summary>
        /// <value>The workflow steps.</value>
        public IEnumerable<IWorkflowStep<T>> WorkflowSteps
        {
            get
            {
                return _underlyingQueue.ToArray();
            }
        }

        /// <summary>
        /// Gets the number of steps registered in the workflow.
        /// </summary>
        /// <value>The count.</value>
        public int Count
        {
            get
            {
                return _underlyingQueue.Count;
            }
        }

        /// <summary>
        /// Starts the workflow by executing the first step.
        /// </summary>
        public void Start()
        {
            _underlyingQueue.TrimExcess();

            if(Logger.IsDebugEnabled) Logger.DebugFormat("Workflow starting with {0} steps registered", _underlyingQueue.Count);

            var firstItem = _underlyingQueue.Dequeue();
            if (firstItem == null) return;

            ExecuteStep(firstItem);
        }

        /// <summary>
        /// Registers the provided step with the workflow.
        /// </summary>
        /// <param name="step">The step.</param>
        public void RegisterStep(IWorkflowStep<T> step)
        {
            RegisterStep(step, ThreadType.Current);
        }

        /// <summary>
        /// Registers the provided step with the workflow on the specified thread in which to execute.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="thread">The thread.</param>
        public void RegisterStep(IWorkflowStep<T> step, ThreadType thread)
        {
            step.Workflow = this;
            step.Thread = thread;
            step.Context = Context;
            step.Completed += Step_Completed;
            step.Failed += Step_Failed;

            _underlyingQueue.Enqueue(step);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow to be executed on the current thread.
        /// </summary>
        /// <param name="step">The step.</param>
        public void RegisterStep(Action<T> step)
        {
            RegisterStep(step, ThreadType.Current);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow on the specified thread in which to execute.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="thread">The thread.</param>
        public void RegisterStep(Action<T> step, ThreadType thread)
        {
            var delegatedStep = new DelegatedWorkflowStep<T>(step) {Context = Context};
            RegisterStep(delegatedStep, thread);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow to be executed on the current thread.
        /// </summary>
        /// <param name="step">The step.</param>
        public void RegisterStep(Action step)
        {
            RegisterStep(step, ThreadType.Current);
        }

        /// <summary>
        /// Registers the provided action as a step in the workflow on the specified thread in which to execute.
        /// </summary>
        /// <param name="step">The step.</param>
        /// <param name="thread">The thread.</param>
        public void RegisterStep(Action step, ThreadType thread)
        {
            var delegatedStep = new DelegatedWorkflowStep<T>(step) { Context = Context };
            RegisterStep(delegatedStep, thread);
        }

        /// <summary>
        /// Stops the workflow execution and raises WorkflowCompleted event.
        /// </summary>
        public void Stop()
        {
            _stopFlagSet = true;

            if(Logger.IsDebugEnabled) Logger.Debug("Workflow forcibly stopped");

            if (WorkflowCompleted != null)
                WorkflowCompleted(this, new WorkflowCompletedEventArgs<T> { Context = Context });
        }

        /// <summary>
        /// Clears all steps from the workflow.
        /// </summary>
        public void Clear()
        {
            _underlyingQueue.Clear();
        }

        #endregion

        #region Protected Members

        /// <summary>
        /// Notifies interested parties that the workflow failed.
        /// </summary>
        /// <param name="eventArgs">The <see cref="Samurai.Workflow.WorkflowCompletedEventArgs&lt;T&gt;"/> instance containing the event data.</param>
        protected void NotifyWorkflowFailed(WorkflowCompletedEventArgs<T> eventArgs)
        {
            if (WorkflowFailed != null)
                WorkflowFailed(this, eventArgs);

            var stepName = (eventArgs.FailedWorkflowStep != null)
                               ? eventArgs.FailedWorkflowStep.GetType().FullName
                               : "Unknown";
            var exceptionDetails = (eventArgs.Exception != null) ? eventArgs.Exception.ToString() : "Unknown";
            Logger.Error(string.Format("Workflow failed on step: {0}\r\nException: {1}", stepName, exceptionDetails));
        }

        /// <summary>
        /// Notifies interested parties that the workflow completed.
        /// </summary>
        /// <param name="eventArgs">The <see cref="Samurai.Workflow.WorkflowCompletedEventArgs&lt;T&gt;"/> instance containing the event data.</param>
        protected void NotifyWorkflowCompleted(WorkflowCompletedEventArgs<T> eventArgs)
        {
            if (WorkflowFailed != null)
                WorkflowFailed(this, eventArgs);

            if(Logger.IsInfoEnabled) Logger.Info("Workflow completed.");
        }

        /// <summary>
        /// Handles the Completed event of the Step control by executing the next step in the workflow.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="Samurai.Workflow.WorkflowStepCompletedEventArgs&lt;T&gt;"/> instance containing the event data.</param>
        protected virtual void Step_Completed(object sender, WorkflowStepCompletedEventArgs<T> e)
        {
            var step = (IWorkflowStep<T>)sender;
            DetachStepEvents(step);

            //If Stop() has been called, we don't want to continue
            if (_stopFlagSet) return;

            if (_underlyingQueue.Count == 0)
            {
                //Queue is empty. Assume workflow is complete.
                if (WorkflowCompleted != null)
                    WorkflowCompleted(this, new WorkflowCompletedEventArgs<T> { Context = Context });

                if(Logger.IsInfoEnabled) Logger.InfoFormat("Step completed: {0}", sender.GetType().FullName);

                return;
            }

            //More workflow steps in the queue...continue exection.
            var nextStep = _underlyingQueue.Dequeue();
            ExecuteStep(nextStep);
        }

        /// <summary>
        /// Handles the Failed event of the Step instance.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="Samurai.Workflow.WorkflowStepCompletedEventArgs&lt;T&gt;"/> instance containing the event data.</param>
        protected virtual void Step_Failed(object sender, WorkflowStepCompletedEventArgs<T> e)
        {
            DetachStepEvents(e.FailedWorkflowStep);

            if (WorkflowFailed != null && e.FailedWorkflowStep.IsCritical)
                NotifyWorkflowFailed(new WorkflowCompletedEventArgs<T>
                                         {
                                             Context = Context,
                                             Exception = e.Exception,
                                             FailedWorkflowStep = e.FailedWorkflowStep
                                         });

            if (e.FailedWorkflowStep.IsCritical || _underlyingQueue.Count <= 0) return;

            //More workflow steps in the queue...continue exection.
            var nextStep = _underlyingQueue.Dequeue();
            ExecuteStep(nextStep);
        }

        #endregion

        private static void ExecuteStep(IWorkflowStep<T> step)
        {
            switch (step.Thread)
            {
                case ThreadType.Current:
                    step.ExecuteStep();
                    break;
                case ThreadType.ThreadPool:
                    ThreadPool.QueueUserWorkItem(x => step.ExecuteStep());
                    break;
                case ThreadType.Ui:
                    ResolveUiThreadDispatcher();

                    if (_dispatcherBeginInvokeMethodInfo == null)
                    {
                        //Not a WPF app, so execute on the current thread
                        Trace.WriteLine("No WPF application present from which to use UI thread. Defaulting to ThreadType.Current.");
                        step.ExecuteStep();
                        return;
                    }

                    var threadStart = new ThreadStart(step.ExecuteStep);
                    var parameters = new[] { threadStart, null };
                    _dispatcherBeginInvokeMethodInfo.Invoke(_uiThreadDispatcher, parameters);

                    break;
                default:
                    throw new InvalidOperationException("BeginWork(), firstItem.ThreadToExecuteOn");
            }
        }

        private static void ResolveUiThreadDispatcher()
        {
            if(_dispatcherBeginInvokeMethodInfo != null) return;

            const string applicationTypeName = "System.Windows.Application";
            const string presentationFrameworkAssembly = "PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35";
            const string applicationInstancePath = "Current";
            const string dispatcherPath = "Dispatcher";

            var applicationType = Type.GetType(string.Format("{0}, {1}", applicationTypeName, presentationFrameworkAssembly), false, false);
            var currentPropertyInfo = applicationType.GetProperty(applicationInstancePath, (BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy));
            var app = currentPropertyInfo.GetValue(null, null);

            if (app == null) return;

            var dispatcherPropertyInfo = applicationType.GetProperty(dispatcherPath);
            _uiThreadDispatcher = dispatcherPropertyInfo.GetValue(app, null);

            if (_uiThreadDispatcher == null) return;

            _dispatcherBeginInvokeMethodInfo = _uiThreadDispatcher.GetType().GetMethod("BeginInvoke", new[] { typeof(Delegate), typeof(object[])});
        }

        private void DetachStepEvents(IWorkflowStep<T> step)
        {
            step.Completed -= Step_Completed;
            step.Failed -= Step_Failed;
        }
    }
}
